这是JS 原生方法原理探究系列的第三篇文章。本文会介绍如何模拟实现 new
操作符。关于 new
的具体用法,MDN 已经描述得很清楚了,这里我们只做简单的介绍,具体的重点在于如何模拟实现。
new 操作符的规范
下面展示的所有规范都是 ES5 版本的,与现在最新的规范有些区别
首先看一下根据规范的描述, new
操作符做了什么事:
全是英文,不过没关系,我简单翻译一下:
我在使用 new
操作符的时候,后面跟着的构造函数可能带参数,也可能不带参数,如果不带参数的话,比如说 new Fn()
,那么这里这个 Fn
就是一个 NewExpression
;如果带参数,比如说 new Fn(name,age)
,那么这里的 Fn
就是一个 MemberExpression
。
这两种情况下使用 new
操作符所进行的操作有点点不同,这里拿带参数的情况说明一下:
- 首先会对
Fn
这个MemberExpression
求值,其结果是指向实际函数对象的一个引用,我们把这个引用作为ref
- 接着调用
GetValue(ref)
进行求值,得到实际的函数对象,把这个对象作为constructor
- 对
Arguments
也就是传进来的参数求值,得到一个参数列表,作为argList
- 如果
constructor
不是对象,则抛出类型错误 - 如果
constructor
没有实现内部的[[Constructor]]
方法,也抛出类型错误 - 调用
constructor
的[[Constructor]]
方法,并将argList
传入作为参数,返回调用结果
从这些描述可以看出,更多的实现细节放在函数的 [[Constructor]]
方法里。那么这个方法具体是做什么用的呢?
[[Constructor]]
的规范
在 JS 中,函数有两种调用方式,一种是正常调用,这将调用函数的内部方法 [[Call]]
,还有一种是通过 new 调用,此时的函数作为一个构造函数,这将调用函数的另一个内部方法 [[Consturct]]
。所以,要实现 new
操作的话,我们得先搞懂 [[Construct]]
内部方法做了什么事。
这里继续看规范是怎么说的:
简单翻译一下:
当通过可能为空的参数列表调用函数 F
的内部方法 [[Construct]]
的时候,会执行如下步骤:
- 让
obj
作为一个新创建的原生对象 - 按照规范指定的,为
obj
设置所有内部方法 - 将
obj
的内部属性[[Class]]
设置为Object
- 传参
prototype
调用函数F
的内部方法[[Get]]
,获取函数的原型对象,作为proto
- 如果
proto
是对象,则将obj
的内部属性[[Prototype]]
设置为proto
- 如果
proto
不是对象,则将obj
的内部属性[[Prototype]]
设置为标准内建的Object
的原型对象 - 调用函数
F
的内部方法Call
,obj
作为调用时的 this 值,此前传给[[Construct]]
的参数列表作为调用时的参数。将调用后得到的结果作为result
- 如果
result
是对象,则将其返回 - 否则,返回
obj
可以说,规范已经讲得很清楚了,简单地说,在 new 一个构造函数的时候,具体会做下面的事情:
- 内部创建一个实例对象,并指定实例对象的原型:
- 如果构造函数的原型是对象,则让实例的
__proto__
等于构造函数的prototype
- 如果构造函数的原型不是对象,则让实例的
__proto__
等于Object
的prototype
- 如果构造函数的原型是对象,则让实例的
- 将实例对象绑定为构造函数中的 this,此前传递进来的参数作为参数,并执行一遍构造函数
- 如果构造函数返回了对象,则将其作为返回值,否则将实例对象作为返回值
代码实现
ES3 版本的实现如下:
function myNew(Fn){
if(typeof Fn != 'function'){
throw new TypeError(Fn + 'is not a constructor')
}
myNew.target = Fn
var instance = {}
// 检测构造函数原型是不是对象
instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype
const returnValue = Fn.apply(instance,Array.prototype.slice.call(arguments,1))
if(typeof returnValue === 'object' && returnValue !== null || typeof returnValue === 'function'){
return returnValue
} else {
return instance
}
}
ES6 版本的实现如下:
function myNew(Fn,...args){
if(typeof Fn != 'function'){
throw new TypeError(Fn + 'is not a constructor')
}
myNew.target = Fn
const instance = {}
// 检测构造函数原型是不是对象
instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype
const returnValue = Fn.call(instance,...args)
return returnValue instanceof Object ? returnValue : instance
}
注意几个要点:
- 当函数是通过 new 调用的时候,
new.target
会指向函数自身,这个“指向”的操作在代码里就是通过myNew.target = Fn
体现的 - 为什么不直接使用
const instance = Object.create(Fn.prototype)
创建实例呢?根据规范,我们在实现 new 的时候,需要检测构造函数的原型是不是对象,如果不是对象,比如说是 null,那么实例的__proto__
会指向 Object 的原型,而这里如果使用了Object.create
,则会导致实例的__proto__
仍然指向 null。网上很多new
的模拟实现直接使用了Object.create
,或者根本没有对构造函数的原型进行类型检查,这是不够严谨的 - 如果无法使用
instanceof
,我们也可以改用typeof Fn.prototype === 'Object' && Fn.prototype !== null
进行判断